#
# -*- coding: utf8 -*-

'''Uses the Plurk service.

This module defines a PlurkProxy class which serves as a proxy object
of the Plurk service. It provides the following interfaces:

Proxy creation:
  plurk = PlurkProxy(api, json_parser) 
    api: use plurk_api_urllib2.PlurkAPI() or something similar.
    json_parser: use simplejson.loads or something similar.

Get another user's public data into a PlurkUser object:
  user = plurk.GetPublicProfile(username)
Login to Plurk and enable functions that requires login:
  user = plurk.Login(username, password)
TODO: complete the interface document.
'''

import datetime
import jsonizer
import logging
import re
from plurk_user import PlurkUser
from plurk_response import PlurkResponseList

class PlurkPost:
  '''A plurk, i.e., a post'''
  def __init__(self, pdict={}):
    self._data = pdict
    self.id = pdict.get('plurk_id', 0)
    # don't parse more until necessary

  def __str__(self):
    result = 'PlurkPost(%d): %s' % (self.id, self._data)
    return result.encode('utf8')

  def __eq__(self, other):
    if other.__class__ != PlurkPost: return False
    return self._data == other._data


class PlurkPostList:
  '''A list of plurks and the set of users that posted them.'''
  def __init__(self, plurk_json_list, user_json_list=[]):
    self._plurks = [PlurkPost(p) for p in plurk_json_list]
    self._users = [PlurkUser(u) for u in user_json_list]
  def __iter__(self):
    return self._plurks
  def GetUsers(self):
    return self._users
  def __eq__(self, other):
    if other.__class__ != PlurkPostList: return False
    if self._plurks != other._plurks: return False
    if self._users != other._users: return False
    return True


class PlurkProxy:
  '''A proxy object of the Plurk service.

  A PlurkProxy object talks to the Plurk service via a PlurkAPI object
  and return the Python objects produced by parsing the returned json string.
  '''
  allowed_qualifiers = ('loves', 'likes', 'shares', 'gives', 'hates', 
      'wants'' has', 'will', 'asks', 'wishes', 'was', 'feels', 'thinks', 
      'says', 'is', ':', 'freestyle', 'hopes', 'needs', 'wonders')

  def __init__(self, api, json_parser, debug_level=0):
    self._api = api
    self._json_parser = json_parser
    self._debug_level = debug_level
    self._status_code = 0  # status code of the last http call
    self._result = {}      # result obtained from the last http call

  def _CallAPI(self, path, encrypt=False, **kw):
    '''Calls PlurkAPI and convert resulting json string to python object.'''
    self._status_code, response_body = self._api(path, encrypt, **kw)
    try:
      self._result = self._json_parser(response_body)
    except:
      self._result = response_body
    self._error_text = ''
    if self._status_code != 200:
      if type(self._result) == type({}) and self._result.has_key('error_text'):
        self._error_text = self._result['error_text']
    return self._status_code, self._result

  def _MakeOptions(self, kw, conv_table):
    '''Convert Python kw dict to Plurk options dict for passing to Plurk.'''
    options = {}
    for k in conv_table.keys():
      if kw.has_key(k): options[k] = conv_table[k](kw[k])
    return options

  def GetStatusCode(self):
    '''Get the status code of the last http call'''
    return self._status_code

  def GetResult(self):
    '''Get a shallow copy of the last http result.'''
    import copy
    return copy.copy(self._result)

  def GetErrorText(self):
    '''Get the error_text returned from Plurk when the status code != 200.'''
    return self._error_text

  # ----  User  ----

  def Register(self, nick_name, full_name, password, gender, date_of_birth,
      email=''):
    '''Get the profile page of a user by username.
    Args:
      nick_name: [string] The user's nick name. Should be longer than 3 
          characters. Should be ASCII. Nick name can only contain letters, 
          numbers and _.
      full_name: [string] Can't be empty.
      password: [string] Should be longer than 3 characters.
      gender: [string] Should be male or female.
      date_of_birth: [datetime.date]
    Optional args:
      email: [string] Must be a valid email.
    Returns:
      A PlurkUser object for the newly registerd user or None if it failed.
    '''
    options = {'nick_name': nick_name, 'full_name': full_name,
        'password': password, 'gender': gender,
        'date_of_birth': jsonizer.conv_date(date_of_birth)}
    if email: options['email'] = email
    status_code, result = self._CallAPI('/Users/register',
        encrypt=True, **options)
    return None if status_code != 200 else PlurkUser(result)

  def GetPublicProfile(self, username):
    '''Get the profile page of a user by username.
    Args:
      username: [string] the username.
    Returns:
      A PlurkUser object containing the public data of the user
      or None if it failed.
    '''
    status_code, result = self._CallAPI(
        '/Profile/getPublicProfile', user_id=username)
    return None if status_code != 200 else PlurkUser(result)

  def Login(self, username, password):
    '''Login for an existing user and return a PlurkUser object for the user.
    Args:
      username: [string] the username.
      password: [string] the password.
    Returns:
      a PlurkUser object if login succeeded, or None if it fails.
      Use GetErrorText() to find the reason of failure.
    '''
    status_code, result = self._CallAPI(
        '/Users/login', encrypt=True, username=username, password=password)
    return None if status_code != 200 else PlurkUser(result)

  def UpdateUserInfo(self, password, **kw):
    '''Updates the user's information. Requires login.
    Args:
      password: the user's password.
    Optional args: items to update.
      full_name: [string], the new full name to use.
      new_password: [string], the new password to set.
      email: [string], change email.
      display_name: [string], user's display name, can be empty and full
                    unicode. Must be shorter than 15 characters.
      privacy: [string], user's privacy settings. The option can be 
                    'world' (whole world can view the profile),
                    'only_friends' (only friends can view the profile) or 
                    'only_me' (only the user can view own plurks).
      date_of_birth: [datetime.date], the date of birth to set to.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    options = self._MakeOptions(kw, 
        {'full_name': jsonizer.identity, 'new_password': jsonizer.identity,
         'email': jsonizer.identity, 'display_name': jsonizer.identity,
         'privacy': jsonizer.identity, 'date_of_birth': jsonizer.conv_date})
    status_code, result = self._CallAPI('/Users/update',
        encrypt=True, current_password=password, **options)
    return None if status_code != 200 else result

  def GetOwnProfile(self):
    '''Get the user's own profile. Requires login.
    Args: None.
    Returns:
      The user's profile data.
    '''
    status_code, result = self._CallAPI('/Profile/getOwnProfile')
    return None if status_code != 200 else PlurkUser(result)

  def UploadPicture(self, image):
    raise NotImplementedError

  def UpdatePicture(self, image):
    raise NotImplementedError

  # ----  Plurks  ----

  def GetUnreadCount(self):
    '''Get the count of unread plurk on a user's timeline.
    Args: None.
    Returns:
      A dictionary with keys 'all', 'my', 'private, 'responded' or None
    '''
    status_code, result = self._CallAPI('/Polling/getUnreadCount')
    return None if status_code != 200 else result

  def GetUnreadPlurks(self, **kw):
    '''Get new plurks since the specified time.
    Optional args:
      before: [datetime.datetime] get unread plurks before this timestamp.
      max: [int] return no more than this many plurks.
    Returns:
      A PlurkPostList object or None.
    '''
    tmp = self._MakeOptions(kw,
        {'before': jsonizer.conv_datetime, 'max': jsonizer.identity})
    options = {'offset': tmp['before'], 'limit': tmp['max']}
    status_code, result = self._CallAPI('/Timeline/getUnreadPlurks', **options)
    return None if status_code != 200 else \
           PlurkPostList(result['plurks'], result['plurk_users'].values())

  def GetNewPlurks(self, since):
    '''Get new plurks since the specified time.
    Args:
      since: [datetime.datetime] the timestamp criterion.
    Returns:
      A PlurkPostList object or None.
    '''
    offset = jsonizer.conv_datetime(since)
    status_code, result = self._CallAPI('/Polling/getPlurks', offset=offset)
    return None if status_code != 200 else \
           PlurkPostList(result['plurks'], result['plurk_users'].values())

  def GetPlurks(self, **kw):
    '''Get user's own plurk posts newer than the offset. Requires login.
    Optional Args:
      offset: [datetime.datetime] Return plurks older than offset.
      limit: [int] How many plurks should be returned? Default is 20.
      only_user: [int] The ID of the user who's plurks should be returned.
      only_responded: [bool] only return responded plurks if true.
      only_private: [bool] only return private plurks if true.
    Returns:
      A PlurkPostList object or None.
    '''
    options = self._MakeOptions(kw,
      {'offset': jsonizer.conv_datetime, 'limit': jsonizer.identity,
       'only_user': jsonizer.identity, 'only_responded': jsonizer.conv_bool,
       'only_private': jsonizer.conv_bool})
    status_code, result = self._CallAPI('/Timeline/getPlurks', **options)
    return None if status_code != 200 else \
           PlurkPostList(result['plurks'], result['plurk_users'].values())

  def GetPlurk(self, plurk_id):
    '''Get a plurk by its ID.
    Args:
      plurk_id: [int] the unique ID of the plurk to get.
    Returns:
      A PlurkPostList object or None.
    '''
    status_code, result = self._CallAPI('/Timeline/getPlurk', plurk_id=plurk_id)
    return None if status_code != 200 else \
           PlurkPostList([result['plurk']], [result['user']])

  def MutePlurks(self, plurk_ids):
    '''Mute a list of plurks.
    Args:
      plurk_ids: [list] the list of IDs of the plurk to mute.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    status_code, result = self._CallAPI('/Timeline/mutePlurk', ids=plurk_ids)
    return None if status_code != 200 else result

  def UnmutePlurks(self, plurk_ids):
    '''Unmute a list of plurks.
    Args:
      plurk_ids: [list] the list of IDs of the plurk to mute.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    status_code, result = self._CallAPI('/Timeline/unmutePlurk', ids=plurk_ids)
    return None if status_code != 200 else result

  def MarkAsRead(self, plurk_ids, **kw):
    '''Mark a list of plurks as read.
    Args:
      plurk_ids: [list] the list of IDs of the plurk to mute.
    Optional args:
      note_position: update responses_seen of the plurks if true.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    options = self._MakeOptions(kw, {'note_position': jsonizer.conv_bool})
    status_code, result = self._CallAPI('/Timeline/unmutePlurk',
        ids=plurk_ids, **options)
    return None if status_code != 200 else result

  def AddPlurk(self, qualifier, content, **kw):
    '''Add a plurk.
    Args:
      qualifier: [string] The Plurk's qualifier, must be in English.
      content: [string] The Plurk's text.
    Optional args:
      limited_to: [list] list of friend user IDs that the plurk is visible to.
      no_comments: [int] no responses if set to 1, friend responses if set to 2.
      lang: [string] The plurk's language. Can be following: Show example data
    Returns:
      A PlurkPost object of the new plurk.
    '''
    allowed_langs = {'en': 'English', 'pt_BR': 'Português', 'cn': '中文 (中国)',
        'ca': 'Català', 'el': 'Ελληνικά', 'dk': 'Dansk', 'de': 'Deutsch', 
        'es': 'Español', 'sv': 'Svenska', 'nb': 'Norsk bokmål', 'hi': 'Hindi', 
        'ro': 'Română', 'hr': 'Hrvatski', 'fr': 'Français', 'ru': 'Pусский',
        'it': 'Italiano ', 'ja': '日本語', 'he': 'עברית', 'hu': 'Magyar',
        'ne': 'Nederlands', 'th': 'ไทย', 'ta_fp': 'Filipino', 
        'in': 'Bahasa Indonesia', 'pl': 'Polski', 'ar': 'العربية', 
        'fi': 'Finnish', 'tr_ch': '中文 (繁體中文)', 'tr': 'Türkçe', 
        'ga': 'Gaeilge', 'sk': 'Slovenský', 'uk': 'українська', 'fa': 'فارسی'} 
    self._status_code = -1
    if qualifier not in self.allowed_qualifiers:
      self._error_text = 'Invalid qualifier'
      return None
    if kw.has_key('lang') and kw['lang'] not in allowed_langs.keys():
      self._error_text = 'Invalid lang'
      return None
    options = self._MakeOptions(kw, {'limited_to': jsonizer.identity,
      'no_comments': jsonizer.identity, 'lang': jsonizer.identity})
    status_code, result = self._CallAPI('/Timeline/plurkAdd',
        qualifier=qualifier, content=content, **options)
    return None if status_code != 200 else PlurkPost(result)

  def DeletePlurk(self, plurk_id):
    '''Add a plurk.
    Args:
      plurk_id: [int] the unique ID of the plurk to get.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    status_code, result = self._CallAPI('/Timeline/plurkDelete',
        plurk_id=plurk_id)
    return None if status_code != 200 else result

  def UpdatePlurk(self, plurk_id, content):
    '''Edit a plurk.
    Args:
      plurk_id: [int] the unique ID of the plurk to get.
      content: [string] the new content of plurk.
    Returns:
      A PlurkPost object of the updated plurk.
    '''
    status_code, result = self._CallAPI('/Timeline/plurkDelete',
        plurk_id=plurk_id)
    return None if status_code != 200 else result

  # ----  Response  ----

  def GetResponses(self, plurk_id, offset):
    '''Get responses to a plurk.
    Args:
      plurk_id: [int] the unique ID of the plurk to get responses from.
      offset: [int] the offset for the first response to return.
    Returns:
      A PlurkResponseList object.
    '''
    status_code, result = self._CallAPI('/Responses/get',
        plurk_id=plurk_id, from_responses=offset)
    return None if status_code != 200 else PlurkResponseList(result)

  def AddResponse(self, plurk_id, qualifier, content):
    '''Add a response to a plurk.
    Args:
      plurk_id: [int] the unique ID of the plurk to get responses from.
      qualifier: [string] The response's qualifier, must be in English.
      content: [string] The response's text.
    Returns:
      A PlurkResponse object of the response sent.
    '''
    status_code, result = self._CallAPI('/Responses/responseAdd',
        plurk_id=plurk_id, qualifier=qualifier, content=content)
    return None if status_code != 200 else PlurkResponse(result)

  def DeleteResponse(self, plurk_id, response_id):
    '''Delete a response of a plurk.
    Args:
      plurk_id: [int] the unique ID of the plurk to check responses from.
      response_id: [int] The unique ID of the response to delete.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    status_code, result = self._CallAPI('/Responses/responseDelete',
        plurk_id=plurk_id, response_id=response_id)
    return None if status_code != 200 else result

  # ----  Friends  ----

  def GetFriendsByOffset(self, user_id, offset=0):
    '''Get 10 friends of a user.
    Args:
      user_id: [int] the unique ID of the user to get friends of.
    Optional args:
      offset: [int] the offset for the first friend to return, start from 0.
    Returns:
      A list of PlurkUsers.
    '''
    status_code, result = self._CallAPI('/FriendsFans/getFriendsByOffset',
        user_id=user_id, offset=offset)
    return None if status_code != 200 else [PlurkUser(u) for u in result]

  def GetFriendNames(self):
    '''Get nick name and full name of every friend of the logged in user.
    Returns:
      A dict of user_id to {'nick_name': *, 'full_name': *} or None.
    '''
    status_code, result = self._CallAPI('/FriendsFans/getCompletion')
    return None if status_code != 200 else result

  def MakeFriendWith(self, user_id):
    '''Request to make friend with a user.
    Args:
      user_id: [int] the unique ID of the user to make friend with.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    status_code, result = self._CallAPI('/FriendsFans/becomeFriend',
        friend_id=user_id)
    return None if status_code != 200 else result

  def BreakUpWith(self, user_id):
    '''Remove user user_id as a friend. User user_id won't be notified.
    Args:
      user_id: [int] the unique ID of the user to break up with.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    status_code, result = self._CallAPI('/FriendsFans/removeAsFriend',
        friend_id=user_id)
    return None if status_code != 200 else result

  # ----  Fans and followings  ----

  def GetFansByOffset(self, user_id, offset=0):
    '''Get 10 fans of a user.
    Args:
      user_id: [int] the unique ID of the user to get fans of.
    Optional arg:
      offset: [int] the offset for the first fan to return, start from 0.
    Returns:
      A list of PlurkUsers.
    '''
    status_code, result = self._CallAPI('/FriendsFans/getFansByOffset',
        user_id=user_id, offset=offset)
    return None if status_code != 200 else [PlurkUser(u) for u in result]

  def GetFollowingsByOffset(self, offset=0):
    '''Get 10 users that I follow.
    Optional arg:
      offset: [int] the offset for the first user to return, start from 0.
    Returns:
      A list of PlurkUsers.
    '''
    status_code, result = self._CallAPI('/FriendsFans/getFollowingByOffset',
        offset=offset)
    return None if status_code != 200 else [PlurkUser(u) for u in result]

  def StartFollowing(self, user_id):
    '''Start to follow a user.
    Args:
      user_id: [int] the unique ID of the user to start following.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    status_code, result = self._CallAPI('/FriendsFans/becomeFan',
        fan_id=user_id)
    return None if status_code != 200 else result

  def UpdateFollowing(self, user_id, follow):
    '''Update the following status to a user. The user must already be a friend.
    Args:
      user_id: [int] the unique ID of the user to start following.
      follow: [bool] whether to follow the user or not.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    status_code, result = self._CallAPI('/FriendsFans/setFollowing',
        user_id=user_id, follow=follow)
    return None if status_code != 200 else result

  # ----  Blocked users  ----

  def GetBlocksByOffset(self, offset=0):
    '''Get 10 blocked users by me.
    Optional arg:
      offset: [int] the offset for the first blocked user, start from 0.
    Returns:
      A list of PlurkUsers.
    '''
    status_code, result = self._CallAPI('/FriendsFans/getFansByOffset',
        user_id=user_id, offset=offset)
    return None if status_code != 200 else \
           [PlurkUser(u) for u in result['users']]

  def BlockUser(self, user_id):
    '''Block a user.
    Args:
      user_id: [int] the unique ID of the user to block.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    status_code, result = self._CallAPI('/Blocks/block', user_id=user_id)
    return None if status_code != 200 else result

  def UnblockUser(self, user_id):
    '''Unblock a user.
    Args:
      user_id: [int] the unique ID of the user to unblock.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    status_code, result = self._CallAPI('/Blocks/unblock', user_id=user_id)
    return None if status_code != 200 else result

  # ----  Cliques  ----

  def GetCliqueNames(self):
    '''Get the names of all my cliques.
    Returns:
      A list of clique names.
    '''
    status_code, result = self._CallAPI('/Cliques/get_cliques')
    return None if status_code != 200 else result

  def GetClique(self, clique_name):
    '''Get the users in a clique of mine.
    Args:
      clique_name: [string] the name of the clique to query.
    Returns:
      A list of PlurkUser.
    '''
    status_code, result = self._CallAPI('/Cliques/get_clique',
        clique_name=clique_name)
    return None if status_code != 200 else \
           [PlurkUser(u) for u in result]

  def CreateClique(self, clique_name):
    '''Create a clique.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    status_code, result = self._CallAPI('/Cliques/create_clique',
        clique_name=clique_name)
    return None if status_code != 200 else result

  def RenameClique(self, old_name, new_name):
    '''Rename a clique.
    Args: waiting for official API document
      old_name: [string] old name of the clique to rename.
      new_name: [string] new name of the clique.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    status_code, result = self._CallAPI('/Cliques/rename_clique',
        clique_name=old_name, new_name=new_name)
    return None if status_code != 200 else result

  def DeleteClique(self, clique_name):
    '''Delete a clique.
    Args:
      clique_name: [string] the name of the clique to delete.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    status_code, result = self._CallAPI('/Cliques/delete_clique',
        clique_name=clique_name)
    return None if status_code != 200 else result

  def AddUserToClique(self, clique_name, user_id):
    '''Add a user to a clique.
    Args:
      clique_name: [string] the name of the clique.
      user_id: [int] the unique ID of the user to add to the clique.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    status_code, result = self._CallAPI('/Cliques/add',
        clique_name=clique_name, user_id=user_id)
    return None if status_code != 200 else result

  def DeleteUserInClique(self, clique_name, user_id):
    '''Delete a user from a clique.
    Args:
      clique_name: [string] the name of the cliqu.
      user_id: [int] the unique ID of the user to delete from the clique.
    Returns:
      {"success_text": "ok"} if successful or None.
    '''
    status_code, result = self._CallAPI('/Cliques/remove',
        clique_name=clique_name, user_id=user_id)
    return None if status_code != 200 else result

  # ----  Emoticons  ----

  def GetEmoticons(self):
    '''Get the emoticon URLs and the conditions that they can be shown.
    Returns:
      A PlurkEmoticons object.
    '''
    status_code, result = self._CallAPI('/Emoticons/get')
    return None if status_code != 200 else PlurkEmoticons(result)

  # ----  Search  ----

  def SearchPlurks(self, query, oldest_plurk_id=1):
    '''Search plurks containing the query.
    Args:
      query: [string] the query to search.
    Optional args:
      oldest_plurk_id: [int] the plurk_id of the oldest Plurk to return.
    Returns:
      A PlurkPostList object or None.
    '''
    status_code, result = self._CallAPI('/PlurkSearch/search',
        query=query, offset=oldest_plurk_id)
    return None if status_code != 200 else PlurkPostList(result)

  def SearchUsers(self, query, page_offset=0):
    '''Search users containing the query.
    Args:
      query: [string] the query to search.
    Optional args:
      page_offset: [int] the offset to returned users, like 0, 10, 20, etc.
    Returns:
      A list of PlurkUsers or None.
    '''
    status_code, result = self._CallAPI('/UserSearch/search',
        query=query, offset=page_offset)
    return None if status_code != 200 else \
           [PlurkUser(u) for u in result]

if __name__ == '__main__':
  pass
